using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Charisma.Linq;
using Charisma.MongoDB.Linq;
using Charisma.MongoDb.Linq.Expressions;
using MongoDB.Bson.Serialization;
using Bson = global::MongoDB.Bson.Serialization;

namespace Charisma.MongoDb.Linq
{
  partial class Translator
  {
    public override Expression Visit(Expression e) {
      if (e == null)
        return null;
      if (e.IsProjection())
        return e;
      if (context.Evaluator.CanBeEvaluated(e))
        return typeof (IQueryable).IsAssignableFrom(e.Type)
          ? base.Visit(ExpressionEvaluator.Evaluate(e))
          : ExpressionEvaluator.Evaluate(e);
      return base.Visit(e);
    }

    protected override Expression VisitConstant(ConstantExpression c)
    {
      if (c.Type.Is<IQueryable>()) {
        if (c.Value == null)
          throw new InvalidOperationException();
        var query = (IQueryable)c.Value;
        if (query.Expression == c)
          return BuildRootProjection(query);
      }
      return base.VisitConstant(c);
    }

    protected override Expression VisitMember(MemberExpression e)
    {
      if (context.Evaluator.CanBeEvaluated(e) && context.ParameterExtractor.IsParameter(e)) {
        if (typeof(IQueryable).IsAssignableFrom(e.Type)) {
          var lambda = Expression.Lambda<Func<IQueryable>>(e).Compile();
          var rootPoint = lambda();
          if (rootPoint != null)
            return base.Visit(rootPoint.Expression);
        }
        return e;
      }
      if (e.Expression == null) {
        if (typeof(IQueryable).IsAssignableFrom(e.Type)) {
          var lambda = Expression.Lambda<Func<IQueryable>>(e).Compile();
          var rootPoint = lambda();
          if (rootPoint != null)
            return VisitSequence(rootPoint.Expression);
        }
      }
      else if (e.Expression.NodeType == ExpressionType.Constant) {
        var rfi = e.Member as FieldInfo;
        if (rfi != null && (rfi.FieldType.IsGenericType && typeof(IQueryable).IsAssignableFrom(rfi.FieldType))) {
          var lambda = Expression.Lambda<Func<IQueryable>>(e).Compile();
          var rootPoint = lambda();
          if (rootPoint != null)
            return VisitSequence(rootPoint.Expression);
        }
      }

      var scope = state.BuildingField
        ? null
        : state.CreateScope();
      using (scope) {
        state.BuildingField |= true;
        var source = Visit(e.Expression);
        var result = GetMember(source, e);
        return result ?? Expression.MakeMemberAccess(source, e.Member);
      }
    }

    private Expression GetMember(Expression source, MemberExpression e)
    {
      if (source == null)
        return null;
      source = source.StripCasts();
      if (source.IsAnonymousConstructor()) {
        var newExpression = (NewExpression)source;
        int memberIndex = newExpression.Members.IndexOf(e.Member);
        if (memberIndex < 0)
          throw new InvalidOperationException();
        var argument = Visit(newExpression.Arguments[memberIndex]);
        return argument;
      }
      var mongoExpression = source as MongoExpression;
      if (mongoExpression == null)
        return null;
      switch (mongoExpression.MongoNodeType) {
        case MongoExpressionType.Projection:
        case MongoExpressionType.Collection:
        case MongoExpressionType.Query:
        case MongoExpressionType.Order:
        case MongoExpressionType.Scalar:
        case MongoExpressionType.Aggregate:
        case MongoExpressionType.AggregateScalar:
          throw new NotSupportedException();
        case MongoExpressionType.Field: {
          var parent = (FieldExpression) mongoExpression;
          var parentMetaType = parent.MetaType;
          var metaField = parentMetaType == null ? null : parentMetaType.GetMemberMap(e.Member.Name);
          if (parentMetaType == null || metaField == null)
            return source == e.Expression ? e : Expression.MakeMemberAccess(source, e.Member);
          var metaType = LinqHelper.IsWellKnown(e.Type)
            ? null
            : BsonClassMap.LookupClassMap(e.Type);
          var name = string.Format("{0}.{1}", parent.Name, metaField.ElementName);
          var fieldExpression = new FieldExpression(e.Type, name, metaType, Expression.MakeMemberAccess(source, e.Member));
//          if (scope != null)
//            state.Members.Add(fieldExpression);
          return fieldExpression;
        }
        case MongoExpressionType.Entity: {
          var entity = (EntityExpression) mongoExpression;
          var metaField = entity.MetaType.GetMemberMap(e.Member.Name);
          if (metaField == null)
            return source == e.Expression ? e : Expression.MakeMemberAccess(source, e.Member);
          var metaType = LinqHelper.IsWellKnown(e.Type)
            ? null
            : Bson.BsonClassMap.LookupClassMap(e.Type);
            var fieldExpression = new FieldExpression(e.Type, metaField.ElementName, metaType, Expression.MakeMemberAccess(source, e.Member));
//          if (scope != null)
//            state.Members.Add(fieldExpression);
          return fieldExpression;
        }
        default:
          throw new ArgumentOutOfRangeException();
      }
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
      ProjectionExpression projectionExpression;
      if (context.Bindings.TryGetValue(node, out projectionExpression))
        return projectionExpression.Projector;
      return node;
    }

    private Expression BuildRootProjection(IQueryable query)
    {
      var provider = (QueryProvider) query.Provider;
      var elementType = query.ElementType;
      var element = Expression.Parameter(elementType, "e");
      var colExpression = new CollectionExpression(provider.CollectionName, elementType, element);
      var queryExpression = new QueryExpression(colExpression);
      var entity = BuildEntityExpression(element);
      return new ProjectionExpression(queryExpression, entity, null, ResultType.All);
    }

    private EntityExpression BuildEntityExpression(ParameterExpression element)
    {
      Contract.Requires(element != null);
      
      var metaType = BsonClassMap.LookupClassMap(element.Type);
      return new EntityExpression(element, metaType);
    }

    private ProjectionExpression VisitSequence(Expression source)
    {
      var visited = Visit(source);
      return (ProjectionExpression) visited;
    }
  }
}